/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Library General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 * SPIDevice.cpp
 * SPI device
 * Copyright (C) 2013 Simon Newton
 */

#include <sstream>
#include <string>
#include <vector>

#include <errno.h>
#include <fcntl.h>

#include <linux/spi/spidev.h>
#include <sys/ioctl.h>

#include "ola/Logging.h"
#include "ola/StringUtils.h"
#include "ola/network/NetworkUtils.h"
#include "ola/network/SocketCloser.h"
#include "olad/PluginAdaptor.h"
#include "olad/Preferences.h"
#include "olad/Universe.h"
#include "plugins/spi/SPIDevice.h"
#include "plugins/spi/SPIPort.h"
#include "plugins/spi/SPIPlugin.h"

namespace ola {
namespace plugin {
namespace spi {

const char SPIDevice::SPI_DEVICE_NAME[] = "SPI Plugin";

const uint16_t SPIDevice::SLOTS_PER_PIXEL = 3;

const uint16_t SPIDevice::SPI_DELAY = 0;
const uint8_t SPIDevice::SPI_BITS_PER_WORD = 8;
const uint8_t SPIDevice::SPI_MODE = 0;

const string SPIDevice::MUX_MODE_SHARED_BUFFER = "shared-buffer";

/*
 * Create a new device
 */
SPIDevice::SPIDevice(SPIPlugin *owner,
                     Preferences *prefs,
                     PluginAdaptor *plugin_adaptor,
                     const string &spi_device,
                     ola::rdm::UIDAllocator &uid_allocator)
    : Device(owner, SPI_DEVICE_NAME),
      m_preferences(prefs),
      m_plugin_adaptor(plugin_adaptor),
      m_spi_device_name(spi_device),
      m_spi_device_path(spi_device),
      m_spi_speed(100000),
      m_fd(-1)
{
  size_t pos = spi_device.find_last_of("/");
  if (pos != string::npos)
    m_spi_device_name = spi_device.substr(pos + 1);

  set<string> mux_modes;
  mux_modes.insert(MUX_MODE_SHARED_BUFFER);

  // 2048 is a reasonable maximum for updating at 60fps
  m_preferences->SetDefaultValue(PixelCountKey(), IntValidator(0, 2048), "170");
  m_preferences->SetDefaultValue(SPISpeedKey(), IntValidator(0, 32000000), "2000000");
  m_preferences->SetDefaultValue(MuxModeKey(), SetValidator(mux_modes), MUX_MODE_SHARED_BUFFER);
  // Default to 1 universe of pixels. There isn't really any problem with sending more data than necessary
  m_preferences->SetDefaultValue(PixelsPerPortKey(), IntValidator(1, 170), "170");

  // Read multiplexing info
  m_mux_mode = m_preferences->GetValue(MuxModeKey());
  StringToInt(m_preferences->GetValue(PixelCountKey()), &m_total_pixel_count);
  StringToInt(m_preferences->GetValue(PixelsPerPortKey()), &m_pixels_per_port);

  // Calculate the buffer size
  m_shared_spi_buffer_size = m_total_pixel_count * SPIDevice::SLOTS_PER_PIXEL;

  OLA_INFO << m_spi_device_name << " is configured for " << m_total_pixel_count << " pixels with " << m_pixels_per_port << " per port using " << m_mux_mode << " multiplexing mode.";

  // Read the max SPI speed
  int spi_speed;
  if (StringToInt(m_preferences->GetValue(SPISpeedKey()), &spi_speed)) {
    m_spi_speed = spi_speed;
  }

  m_port_count = 0;

  // Divide up the pixels into ports with up to pixels_per_port pixels each
  for (int offset = 0,
          port_index = 0;
      offset < m_total_pixel_count;
      offset += m_pixels_per_port,
          port_index ++,
          m_port_count ++
  ) {
    SPIOutputPort::SPIPortOptions settings;

    settings.pixel_offset = offset;

    if (m_total_pixel_count - offset > m_pixels_per_port) {
      settings.pixel_count = m_pixels_per_port;
    } else {
      settings.pixel_count = m_total_pixel_count - offset;
    }

    SPIOutputPort* port = new SPIOutputPort(this, port_index, m_spi_device_name, *uid_allocator.AllocateNext(), settings);

    OLA_INFO << "Created port " << port->Description() << " for pixels " << (settings.pixel_offset) << " to " << (settings.pixel_offset + settings.pixel_count) << " of " << m_spi_device_name;

    settings.spi_speed = m_spi_speed;

    AddPort(port);
  }
}

string SPIDevice::DeviceId() const {
  return m_spi_device_name;
}

/**
 * Initializes the SPI device.
 *
 * Returns true if the device was initialized successfully, false otherwise.
 */
bool SPIDevice::InitSPI() {
  if (m_fd == -1) {
    // Only initialize once

    int fd = open(m_spi_device_path.c_str(), O_RDWR);
    ola::network::SocketCloser closer(fd);

    if (fd < 0) {
      OLA_WARN << "Failed to open " << m_spi_device_name << " : " << strerror(errno);
      return false;
    }

    uint8_t spi_mode = SPIDevice::SPI_MODE;
    if (ioctl(fd, SPI_IOC_WR_MODE, &spi_mode) < 0) {
      OLA_WARN << "Failed to set SPI_IOC_WR_MODE for " << m_spi_device_name;
      return false;
    }

    uint8_t spi_bits_per_word = SPIDevice::SPI_BITS_PER_WORD;
    if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &spi_bits_per_word) < 0) {
      OLA_WARN << "Failed to set SPI_IOC_WR_BITS_PER_WORD for " << m_spi_device_name;
      return false;
    }

    if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &m_spi_speed) < 0) {
      OLA_WARN << "Failed to set SPI_IOC_WR_MAX_SPEED_HZ for " << m_spi_device_name;
      return false;
    }

    if (m_mux_mode == MUX_MODE_SHARED_BUFFER) {
      // Allocate the needed buffer for all pixels
      m_shared_spi_buffer = (uint8_t*) malloc(m_shared_spi_buffer_size);
      memset(m_shared_spi_buffer, 0, m_shared_spi_buffer_size);
    }

    m_fd = closer.Release();
  }

  return true;
}

void SPIDevice::WriteToSPIBuffer(unsigned int port_id, unsigned int byte_count, const uint8_t* data) {
  // Ensure we're initialized
  if (m_fd < 0) return;

  if (m_mux_mode == MUX_MODE_SHARED_BUFFER) {
    unsigned int byte_offset = port_id * m_pixels_per_port * SLOTS_PER_PIXEL;

    // Check for out-of-bunds offset
    if ((int)byte_offset >= m_shared_spi_buffer_size) return;

    // Trim data if there is more than we support
    if ((int)(byte_offset + byte_count) > m_shared_spi_buffer_size) {
      byte_count = m_shared_spi_buffer_size - byte_offset;
    }

    memcpy(m_shared_spi_buffer + byte_offset, data, byte_count);

    // Only flush for the last port, thereby avoiding partially written frames
    if ((int) port_id == m_port_count - 1) {
      FlushSharedSPIBuffer();
    }
  }
}


void SPIDevice::FlushSharedSPIBuffer() const {
  if (m_fd >= 0) {
    struct spi_ioc_transfer spi;
    memset(&spi, 0, sizeof(spi));

    spi.tx_buf = reinterpret_cast<__u64>(m_shared_spi_buffer);
    spi.len = m_shared_spi_buffer_size;
    int WritePixelDataForPort = ioctl(m_fd, SPI_IOC_MESSAGE(1), &spi);
    if (WritePixelDataForPort != static_cast<int>(m_shared_spi_buffer_size)) {
      OLA_WARN << "Failed to write all the SPI data (wrote " << WritePixelDataForPort << " of " << spi.len << "): " << strerror(errno);
    }
  }
}

/*
 * Start this device
 */
bool SPIDevice::StartHook() {
  for (int port_id = 0; port_id < m_port_count; port_id++) {
    SPIOutputPort *port = (SPIOutputPort*) GetOutputPort(port_id);

    if (!port->Init())
      return false;

    uint8_t personality;
    if (StringToInt(m_preferences->GetValue(PersonalityKey(port->PortId())), &personality)) {
      port->SetPersonality(personality);
    }

    uint16_t dmx_address;
    if (StringToInt(m_preferences->GetValue(StartAddressKey(port->PortId())), &dmx_address)) {
      port->SetStartAddress(dmx_address);
    }
  }

  return true;
}


void SPIDevice::PrePortStop() {
  stringstream str;

  // Save configuration for each port
  for (int port_id = 0; port_id < m_port_count; port_id++) {
    SPIOutputPort *port = (SPIOutputPort*) GetOutputPort(port_id);

    str << static_cast<int> (port->GetPersonality());
    m_preferences->SetValue(PersonalityKey(port->PortId()), str.str());
    str.str("");

    str << port->GetStartAddress();
    m_preferences->SetValue(StartAddressKey(port->PortId()), str.str());
    str.str("");
  }

  m_preferences->Save();

  // Clean up resources
  if (m_fd >= 0) {
    close(m_fd);
    m_fd = -1;

    if (m_mux_mode == MUX_MODE_SHARED_BUFFER) {
      free(m_shared_spi_buffer);
    }
  }
}


string SPIDevice::PersonalityKey(int port_id) const {
  std::ostringstream s;
  s << m_spi_device_name << "-port-" << port_id << "-personality";
  return s.str();
}

string SPIDevice::StartAddressKey(int port_id) const {
  std::ostringstream s;
  s << m_spi_device_name << "-port-" << port_id << "-dmx-address";
  return s.str();
}

string SPIDevice::SPISpeedKey() const {
  return m_spi_device_name + "-spi-speed";
}

string SPIDevice::MuxModeKey() const {
  return m_spi_device_name + "-mux-mode";
}

string SPIDevice::PixelsPerPortKey() const {
  return m_spi_device_name + "-pixels-per-port";
}

string SPIDevice::PixelCountKey() const {
  return m_spi_device_name + "-pixel-count";
}
}  // spi
}  // plugin
}  // ola
